Subsystem 3a: team chat bridge (two-way #teamchat ↔ in-game) - #8
Merged
Conversation
…t seam Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Unlike ConnectAsync/GetInfoAsync which use broad catches to return typed outcomes, SendTeamMessageAsync intentionally lets exceptions propagate so the upstream supervisor can classify them as a failed send result. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ChatSender Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ose race Add an early _disposed check in PublishTeamMessageAsync to prevent reading _shutdown.Token after DisposeAsync has disposed it. Also add explanatory comments on the shutdown-token choice and the fire-and-forget pattern.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…imeouts Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds IWorkspaceStore.GetChannelsByKeyAsync and its EF Core implementation for a cross-guild reverse lookup of ProvisionedChannel rows by ChannelKey, needed by the upcoming team-chat channel locator (subsystem 3a Task 6). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
FakeWorkspaceStore was missing the new IWorkspaceStore member added in the previous commit, causing the Workspace.Tests assembly to fail to build (CS0535) and silently skip all 38 tests. Adds the in-memory implementation filtered by ChannelKey over _channels.Values. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… initializers Add Cache_refreshes_after_ttl_expires test proving the locator serves stale cache within the 30 s TTL and rebuilds after expiry. Change the two explicit Dictionary<...> field initializers to target-typed new(). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…intent Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…bridge Add ## Discord application setup (team chat bridge) section to running-locally.md explaining the required privileged gateway intent and Manage Webhooks permission. Apply jb ReformatAndReorder to all chat-bridge source files (these were never jb-formatted before this finalization pass). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Implements the first slice of “Subsystem 3a” by adding a two-way bridge between in-game team chat and a per-server Discord #teamchat channel. This introduces a new Chat feature project, wires team-chat receive/send through Connections, and adds Workspace support to provision and locate the #teamchat channel.
Changes:
- Adds a new
RustPlusBot.Features.Chatproject that relays game → Discord via per-player webhooks and Discord → game via a hostedMessageReceivedlistener, with loop prevention using a sharedRelayDedupBuffer. - Extends Connections to publish
TeamMessageReceivedEventfrom the live socket and to expose anITeamChatSenderseam for sending messages to team chat. - Adds Workspace support for the per-server
#teamchatchannel and a TTL-cachedITeamChatChannelLocator, plus required Discord gateway intents and updated local-dev docs.
Reviewed changes
Copilot reviewed 42 out of 42 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/RustPlusBot.Persistence.Tests/Workspace/WorkspaceStoreByKeyTests.cs | Adds coverage for GetChannelsByKeyAsync persistence query behavior. |
| tests/RustPlusBot.Features.Workspace.Tests/Specs/ServerWorkspaceSpecProviderTests.cs | Verifies the server workspace spec contributes the teamchat channel. |
| tests/RustPlusBot.Features.Workspace.Tests/Locating/TeamChatChannelLocatorTests.cs | Adds coverage for teamchat channel lookup and TTL cache behavior. |
| tests/RustPlusBot.Features.Workspace.Tests/Fakes/FakeWorkspaceStore.cs | Updates fake store to support GetChannelsByKeyAsync. |
| tests/RustPlusBot.Features.Connections.Tests/TeamChatSenderTests.cs | Adds tests for publish-on-receive and send-to-socket routing. |
| tests/RustPlusBot.Features.Connections.Tests/Fakes/FakeRustSocketSource.cs | Extends fake connection to support team message send/receive simulation. |
| tests/RustPlusBot.Features.Chat.Tests/TeamChatRelayTests.cs | Adds unit tests for game → Discord relay and echo dropping. |
| tests/RustPlusBot.Features.Chat.Tests/TeamChatInboundProcessorTests.cs | Adds unit tests for Discord → game filtering/formatting/sending. |
| tests/RustPlusBot.Features.Chat.Tests/RustPlusBot.Features.Chat.Tests.csproj | Introduces the Chat test project. |
| tests/RustPlusBot.Features.Chat.Tests/RelayDedupBufferTests.cs | Adds unit tests for dedup buffer semantics and TTL expiry. |
| tests/RustPlusBot.Features.Chat.Tests/ChatRegistrationTests.cs | Validates DI registration for Chat services (but see stored comment). |
| tests/RustPlusBot.Abstractions.Tests/Events/TeamMessageReceivedEventTests.cs | Adds unit test coverage for the new event type. |
| src/RustPlusBot.Persistence/Workspace/WorkspaceStore.cs | Implements GetChannelsByKeyAsync query. |
| src/RustPlusBot.Persistence/Workspace/IWorkspaceStore.cs | Adds GetChannelsByKeyAsync to the workspace store contract. |
| src/RustPlusBot.Host/RustPlusBot.Host.csproj | References the new Chat feature project from the host. |
| src/RustPlusBot.Host/Program.cs | Registers AddChat() in the host DI setup. |
| src/RustPlusBot.Features.Workspace/WorkspaceServiceCollectionExtensions.cs | Registers ITeamChatChannelLocator as a singleton. |
| src/RustPlusBot.Features.Workspace/WorkspaceKeys.cs | Adds stable key for per-server teamchat channel. |
| src/RustPlusBot.Features.Workspace/Specs/ServerWorkspaceSpecProvider.cs | Provisions per-server #teamchat as an interactive channel. |
| src/RustPlusBot.Features.Workspace/Locating/TeamChatChannelLocator.cs | Adds TTL-cached resolver for per-server teamchat channels. |
| src/RustPlusBot.Features.Workspace/Locating/ITeamChatChannelLocator.cs | Introduces public locator interface for cross-feature use. |
| src/RustPlusBot.Features.Workspace/Localization/LocalizationCatalog.cs | Adds EN/FR localization keys for channel.teamchat.name. |
| src/RustPlusBot.Features.Connections/Supervisor/ConnectionSupervisor.cs | Adds inbound team-chat event publishing and ITeamChatSender implementation. |
| src/RustPlusBot.Features.Connections/Listening/TeamChatLine.cs | Adds representation of raw team chat lines from the socket. |
| src/RustPlusBot.Features.Connections/Listening/RustPlusSocketSource.cs | Wires RustPlusApi team chat receive/send into IRustServerConnection. |
| src/RustPlusBot.Features.Connections/Listening/ITeamChatSender.cs | Introduces the sender seam and TeamChatSendResult. |
| src/RustPlusBot.Features.Connections/Listening/IRustServerConnection.cs | Extends socket interface with team chat send + received event. |
| src/RustPlusBot.Features.Connections/ConnectionServiceCollectionExtensions.cs | Registers ConnectionSupervisor as the singleton backing multiple interfaces. |
| src/RustPlusBot.Features.Chat/Webhooks/ITeamChatWebhookPoster.cs | Adds abstraction for webhook-based posting into Discord teamchat. |
| src/RustPlusBot.Features.Chat/Webhooks/DiscordTeamChatWebhookPoster.cs | Implements webhook discovery/creation and caching (but see stored comment). |
| src/RustPlusBot.Features.Chat/RustPlusBot.Features.Chat.csproj | Introduces the Chat feature project and dependencies. |
| src/RustPlusBot.Features.Chat/Relaying/TeamChatRelay.cs | Implements game → Discord relay with echo drop behavior. |
| src/RustPlusBot.Features.Chat/Relaying/RelayDedupBuffer.cs | Implements short-lived dedup buffer for loop prevention. |
| src/RustPlusBot.Features.Chat/Inbound/TeamChatInboundProcessor.cs | Implements Discord → game processing, formatting, and sending. |
| src/RustPlusBot.Features.Chat/Inbound/InboundOutcome.cs | Adds outcome enum for inbound processing results. |
| src/RustPlusBot.Features.Chat/Inbound/InboundMessage.cs | Adds reduced message DTO for inbound processing. |
| src/RustPlusBot.Features.Chat/Hosting/ChatHostedService.cs | Adds hosted service to consume bus events + listen to Discord messages. |
| src/RustPlusBot.Features.Chat/ChatServiceCollectionExtensions.cs | Adds DI registration for Chat services. |
| src/RustPlusBot.Discord/DiscordServiceCollectionExtensions.cs | Enables GuildMessages + MessageContent gateway intents. |
| src/RustPlusBot.Abstractions/Events/TeamMessageReceivedEvent.cs | Adds new event for publishing received in-game team chat lines. |
| RustPlusBot.slnx | Adds the Chat projects to the solution. |
| docs/development/running-locally.md | Documents Message Content intent + Manage Webhooks requirement. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Comment on lines
+42
to
+47
| // The relay and the inbound processor MUST share the same dedup buffer instance for the | ||
| // record/consume echo pairing to work; the registration uses a single AddSingleton. | ||
| Assert.Same( | ||
| provider.GetRequiredService<RelayDedupBuffer>(), | ||
| provider.GetRequiredService<RelayDedupBuffer>()); | ||
| } |
Comment on lines
+68
to
+73
| var hooks = await channel.GetWebhooksAsync().ConfigureAwait(false); | ||
| var hook = hooks.FirstOrDefault(h => h.Name == WebhookName) | ||
| ?? await channel.CreateWebhookAsync(WebhookName).ConfigureAwait(false); | ||
| var webhookClient = new DiscordWebhookClient(hook); | ||
| return _clients.GetOrAdd(channelId, webhookClient); | ||
| } |
- DiscordTeamChatWebhookPoster: dispose the redundant DiscordWebhookClient when it loses the GetOrAdd cache race (no longer leaks a client/HttpClient). - ChatRegistrationTests: replace the weak twice-resolve singleton assertion with a behavioral test proving the DI-resolved relay and processor share the same RelayDedupBuffer (processor records a line; the relay drops its echo). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Owner
Author
|
Addressed both Copilot comments in e5005b7:
Full suite 149/149, build 0/0, jb-clean. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
First slice of subsystem 3 — a two-way
#teamchat↔ in-game team chat bridge, one channel per registered server. Deliberately ships only the relay (the!commandframework and Discord-side surfaces are deferred to 3b/3c) to de-risk live-socket bidirectional traffic and the cross-feature event fan-out.ConnectionSupervisorsubscribes to each live socket's team-chat event and publishes a newTeamMessageReceivedEventon the in-process bus; a newRustPlusBot.Features.Chat.TeamChatRelayconsumes it, drops the bot's own echoes viaRelayDedupBuffer, resolves the channel viaITeamChatChannelLocator, and posts each line as the player through a per-player webhook (ITeamChatWebhookPoster).ChatHostedServicelistens toMessageReceived;TeamChatInboundProcessorfilters bot/webhook + empty messages, resolves the#teamchatchannel, formats[DisplayName] message, records it for echo-dropping, then relays into team chat via the newITeamChatSenderseam (implemented by the supervisor). Failures get a ❌ reaction.RelayDedupBufferrecords each relayed line and drops only the bot player's matching echo (genuine messages still bridge).#teamchatchannel (Interactive profile) provisioned by the reconciler, EN/FR localized; newITeamChatChannelLocator(TTL-cached, both directions) over a newIWorkspaceStore.GetChannelsByKeyAsync.GuildMessages); docs updated for the dev-portal toggle and the Manage Webhooks permission.New
RustPlusBot.Features.Chatproject mirrors the established per-feature pattern; cross-feature comms go over the event bus + thin public seams (ITeamChatSender,ITeamChatChannelLocator). No new entities or migrations.Architecture notes
RustPlusSocketSourceteam-chat wiring andDiscordTeamChatWebhookPoster. All surrounding logic is unit-tested.Test Plan
dotnet build RustPlusBot.slnx— 0 warnings / 0 errors (strict analyzers)dotnet test RustPlusBot.slnx— 148/148 passing (Abstractions 5, Chat 14, Workspace 42, Pairing 26, Connections 17, Persistence 44)dotnet jb cleanupcode RustPlusBot.slnx --profile=ReformatAndReorder— clean (no changes)ProvisionedChannelrows)🤖 Generated with Claude Code